using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using static LightCAD.MathLib.Constants;

namespace LightCAD.Three
{
    public class Skeleton
    {
        #region scope properties or methods
        //private static Matrix4 _offsetMatrix = new Matrix4();
        private static Matrix4 _identityMatrix = new Matrix4();
        private static SkeletonContext getContext() =>
            ThreeThreadContext.GetCurrThreadContext().SkeletonCtx;
        #endregion

        #region Properties

        public string uuid;
        public ListEx<Bone> bones;
        public ListEx<Matrix4> boneInverses;
        public double[] boneMatrices;
        public DataTexture boneTexture;
        public double boneTextureSize;
        public int frame;

        #endregion

        #region constructor
        public Skeleton(ListEx<Bone> bones = null, ListEx<Matrix4> boneInverses = null)
        {
            this.uuid = MathEx.GenerateUUID();
            this.bones = bones?.Slice(0) ?? new ListEx<Bone>();
            this.boneInverses = boneInverses ?? new ListEx<Matrix4>();
            this.boneMatrices = null;
            this.boneTexture = null;
            this.boneTextureSize = 0;
            this.frame = -1;
            this.init();
        }
        #endregion

        #region methods
        public void init()
        {
            var bones = this.bones;
            var boneInverses = this.boneInverses;
            this.boneMatrices = new double[bones.Length * 16];
            // calculate inverse bone matrices if necessary
            if (boneInverses.Length == 0)
            {
                this.calculateInverses();
            }
            else
            {
                // handle special case
                if (bones.Length != boneInverses.Length)
                {
                    console.warn("THREE.Skeleton: Number of inverse bone matrices does not match amount of bones.");
                    this.boneInverses = new ListEx<Matrix4>();
                    for (int i = 0, il = this.bones.Length; i < il; i++)
                    {
                        this.boneInverses.Push(new Matrix4());
                    }
                }
            }
        }
        public void calculateInverses()
        {
            this.boneInverses.Clear();
            for (int i = 0, il = this.bones.Length; i < il; i++)
            {
                var inverse = new Matrix4();
                if (this.bones[i] != null)
                {
                    inverse.Copy(this.bones[i].matrixWorld).Invert();
                }
                this.boneInverses.Push(inverse);
            }
        }
        public void pose()
        {
            // recover the bind-time world matrices
            for (int i = 0, il = this.bones.Length; i < il; i++)
            {
                var bone = this.bones[i];
                if (bone != null)
                {
                    bone.matrixWorld.Copy(this.boneInverses[i]).Invert();
                }
            }
            // compute the local matrices, positions, rotations and scales
            for (int i = 0, il = this.bones.Length; i < il; i++)
            {
                var bone = this.bones[i];
                if (bone != null)
                {
                    if (bone.parent != null && bone.parent is Bone)
                    {
                        bone.matrix.Copy(bone.parent.matrixWorld).Invert();
                        bone.matrix.Multiply(bone.matrixWorld);
                    }
                    else
                    {
                        bone.matrix.Copy(bone.matrixWorld);
                    }
                    bone.matrix.Decompose(bone.position, bone.quaternion, bone.scale);
                }
            }
        }
        public void update()
        {
            var bones = this.bones;
            var boneInverses = this.boneInverses;
            var boneMatrices = this.boneMatrices;
            var boneTexture = this.boneTexture;
            var _offsetMatrix = getContext()._offsetMatrix;
            // flatten bone matrices to array
            for (int i = 0, il = bones.Length; i < il; i++)
            {
                // compute the offset between the current and the original transform
                var matrix = bones[i] != null ? bones[i].matrixWorld : _identityMatrix;
                _offsetMatrix.MultiplyMatrices(matrix, boneInverses[i]);
                _offsetMatrix.ToArray(boneMatrices, i * 16);
            }
            if (boneTexture != null)
            {
                boneTexture.needsUpdate = true;
            }
        }
        public Skeleton clone()
        {
            return new Skeleton(this.bones, this.boneInverses);
        }
        public Skeleton computeBoneTexture()
        {
            // layout (1 matrix = 4 pixels)
            //      RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
            //  with  8x8  pixel texture max   16 bones * 4 pixels =  (8 * 8)
            //       16x16 pixel texture max   64 bones * 4 pixels = (16 * 16)
            //       32x32 pixel texture max  256 bones * 4 pixels = (32 * 32)
            //       64x64 pixel texture max 1024 bones * 4 pixels = (64 * 64)
            var sized = Math.Sqrt(this.bones.Length * 4); // 4 pixels needed for 1 matrix
            var size = (int)MathEx.CeilPowerOfTwo(sized);
            size = Math.Max(size, 4);
            var boneMatrices = new double[size * size * 4]; // 4 floats per RGBA pixel
            boneMatrices.set(this.boneMatrices); // copy current values
            var boneTexture = new DataTexture(boneMatrices, size, size, RGBAFormat, FloatType);
            boneTexture.needsUpdate = true;
            this.boneMatrices = boneMatrices;
            this.boneTexture = boneTexture;
            this.boneTextureSize = size;
            return this;
        }
        public Bone getBoneByName(string name)
        {
            for (int i = 0, il = this.bones.Length; i < il; i++)
            {
                var bone = this.bones[i];
                if (bone.name == name)
                {
                    return bone;
                }
            }
            return null;
        }
        public void dispose()
        {
            if (this.boneTexture != null)
            {
                this.boneTexture.dispose();
                this.boneTexture = null;
            }
        }
        #endregion
    }
}
